{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0f3f9a5b",
   "metadata": {},
   "source": [
    "# Inspektor Gadget Performance Benchmarks\n",
    "\n",
    "This document provides an overview of the performance impact that Inspektor\n",
    "Gadget has on the system. The document contains different experiments showing\n",
    "different performance aspects.\n",
    "\n",
    "## CPU and Memory Overhead \n",
    "\n",
    "This experiment measures the CPU and memory overhead introduced by Inspektor\n",
    "Gadget (`ig` binary) when running on a host.\n",
    "\n",
    "### Methodology \n",
    "\n",
    "The total total CPU and memory usage of the host is measured while an\n",
    "application generatess a fixed amount of events per second. The experiment is\n",
    "run with and without Inspektor Gadget enabled to compare the overhead. In this\n",
    "scenario, a single gadget is run at the time with the `ig` binary.\n",
    "\n",
    "The memory and CPU are only captured after the gadget is initialized and before\n",
    "it's stopped, so it only accounts for steady state performance."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "92fe81fa",
   "metadata": {},
   "source": [
    "## Results\n",
    "\n",
    "The following cells load and visualize the benchmark results.\n",
    "The produced plots show:\n",
    "- **Host Performance**: Comparison between baseline (without IG) and total system performance with IG enabled.\n",
    "- **IG Process**: Specific overhead introduced by the `ig` process itself (user space only).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5516ad30",
   "metadata": {},
   "source": [
    "### Input File\n",
    "\n",
    "Set the input file path to the CSV file generated by `make benchmarks-test`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b9c1c104",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "# Configuration:\n",
    "# Either set input_file variable or use the environment variable INPUT_FILE\n",
    "#input_file = 'test_results_2025_...csv'\n",
    "input_file = os.environ['INPUT_FILE']\n",
    "\n",
    "# Configuration: Hide code cells when exporting to HTML\n",
    "# Set to True to hide code cells, False to show them\n",
    "hide_code_cells = True"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2117cd25",
   "metadata": {},
   "source": [
    "## Machine Specifications\n",
    "\n",
    "The following section shows the specifications of the machine used for the benchmarks, as recorded in the CSV file."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "418ba852",
   "metadata": {},
   "outputs": [],
   "source": [
    "def extract_machine_specs(filepath):\n",
    "    \"\"\"Extract machine specifications from CSV comment lines\"\"\"\n",
    "    specs = []\n",
    "    try:\n",
    "        with open(filepath, 'r') as file:\n",
    "            for line in file:\n",
    "                line = line.strip()\n",
    "                if line.startswith('#'):\n",
    "                    # Remove the # and any leading/trailing whitespace\n",
    "                    spec_line = line[1:].strip()\n",
    "                    if spec_line:  # Only add non-empty lines\n",
    "                        specs.append(spec_line)\n",
    "                else:\n",
    "                    # Stop reading when we reach the actual CSV data\n",
    "                    break\n",
    "    except Exception as e:\n",
    "        print(f\"Error reading machine specs: {e}\")\n",
    "\n",
    "    return specs\n",
    "\n",
    "# Extract and display machine specifications\n",
    "if input_file and os.path.exists(input_file):\n",
    "    machine_specs = extract_machine_specs(input_file)\n",
    "\n",
    "    if machine_specs:\n",
    "        print(\"Machine Specifications:\")\n",
    "        print(\"=\" * 50)\n",
    "        for spec in machine_specs:\n",
    "            print(f\"• {spec}\")\n",
    "        print(\"=\" * 50)\n",
    "    else:\n",
    "        print(\"No machine specifications found in the CSV file.\")\n",
    "elif input_file:\n",
    "    print(f\"Error: Input file '{input_file}' not found for machine specs\")\n",
    "else:\n",
    "    print(\"No input file specified for machine specs.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ec9c7815",
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "# Import required libraries\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import glob\n",
    "from IPython.display import display, HTML\n",
    "from scipy import stats\n",
    "\n",
    "# Configure HTML output to hide code cells if requested\n",
    "if 'hide_code_cells' in locals() and hide_code_cells:\n",
    "    display(HTML('''\n",
    "    <style>\n",
    "        .jp-CodeCell .jp-Cell-inputWrapper {\n",
    "            display: none !important;\n",
    "        }\n",
    "        .jp-CodeCell .jp-Cell-inputCollapser {\n",
    "            display: none !important;\n",
    "        }\n",
    "        /* For classic notebook interface */\n",
    "        div.input {\n",
    "            display: none !important;\n",
    "        }\n",
    "        div.prompt {\n",
    "            display: none !important;\n",
    "        }\n",
    "    </style>\n",
    "    '''))\n",
    "\n",
    "#from IPython.display import set_matplotlib_formats\n",
    "#set_matplotlib_formats('png', 'pdf')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3a121935",
   "metadata": {},
   "outputs": [],
   "source": [
    "def calculate_stats(data):\n",
    "    \"\"\"Calculate mean and 95% confidence interval for a dataset\"\"\"\n",
    "    if len(data) == 0:\n",
    "        return 0, 0\n",
    "\n",
    "    mean_val = np.mean(data)\n",
    "    if len(data) == 1:\n",
    "        return mean_val, 0\n",
    "\n",
    "    # Calculate 95% confidence interval using t-distribution\n",
    "    sem = stats.sem(data)  # Standard error of the mean\n",
    "    ci = sem * stats.t.ppf((1 + 0.95) / 2, len(data) - 1)\n",
    "\n",
    "    return mean_val, ci"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24cb761f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def aggregate_data(df_gadget):\n",
    "    \"\"\"Aggregate data by test type and events per second, calculating means and CIs\"\"\"\n",
    "    aggregated = []\n",
    "\n",
    "    # Group by TestCase and EventsPerSecond\n",
    "    for (test_name, eps), group in df_gadget.groupby(['TestCase', 'EventsPerSecond']):\n",
    "        cpu_mean, cpu_ci = calculate_stats(group['Cpu'].values)\n",
    "        mem_mean, mem_ci = calculate_stats(group['Mem'].values)\n",
    "        ig_cpu_mean, ig_cpu_ci = calculate_stats(group['IgCpu'].values)\n",
    "        ig_mem_mean, ig_mem_ci = calculate_stats(group['IgMem'].values)\n",
    "        lost_mean, lost_ci = calculate_stats(group['Lost'].values)\n",
    "\n",
    "        aggregated.append({\n",
    "            'Name': test_name.strip(),\n",
    "            'rps': eps,\n",
    "            '%cpu': cpu_mean,\n",
    "            'cpu_ci': cpu_ci,\n",
    "            'mem(MB)': mem_mean,  # Assuming already in MB based on sample data\n",
    "            'mem_ci': mem_ci,\n",
    "            'ig_cpu_mean': ig_cpu_mean,\n",
    "            'ig_cpu_ci': ig_cpu_ci,\n",
    "            'ig_mem_mean': ig_mem_mean,  # Assuming already in MB based on sample data\n",
    "            'ig_mem_ci': ig_mem_ci,\n",
    "            'lost': lost_mean\n",
    "        })\n",
    "\n",
    "    return pd.DataFrame(aggregated)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "235f384a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_performance_table(baseline_data, ig_data):\n",
    "    \"\"\"Create a formatted table with performance metrics\"\"\"\n",
    "    rps_values = baseline_data['rps'].values\n",
    "    baseline_cpu = baseline_data['%cpu'].values\n",
    "    ig_cpu = ig_data['%cpu'].values\n",
    "    baseline_mem = baseline_data['mem(MB)'].values\n",
    "    ig_mem = ig_data['mem(MB)'].values\n",
    "    ig_lost = ig_data['lost'].values\n",
    "\n",
    "    # Get IG process specific data\n",
    "    ig_process_cpu = ig_data['ig_cpu_mean'].values\n",
    "    ig_process_mem = ig_data['ig_mem_mean'].values\n",
    "\n",
    "    # Create table data\n",
    "    table_data = {\n",
    "        'RPS': rps_values,\n",
    "        'Baseline CPU (%)': [f'{cpu:.2f}' for cpu in baseline_cpu],\n",
    "        'With IG CPU (%)': [f'{cpu:.2f}' for cpu in ig_cpu],\n",
    "        'IG Process CPU (%)': [f'{cpu:.2f}' for cpu in ig_process_cpu],\n",
    "        'Baseline Mem (MB)': [f'{mem:.0f}' for mem in baseline_mem],\n",
    "        'With IG Mem (MB)': [f'{mem:.0f}' for mem in ig_mem],\n",
    "        'IG Process Mem (MB)': [f'{mem:.0f}' for mem in ig_process_mem],\n",
    "        'Lost Events': [int(lost) for lost in ig_lost]\n",
    "    }\n",
    "\n",
    "    df_table = pd.DataFrame(table_data)\n",
    "\n",
    "    # Style the table for better display\n",
    "    styled_table = df_table.style.set_table_styles([\n",
    "        {'selector': 'thead th', 'props': [('background-color', '#4CAF50'),\n",
    "                                          ('color', 'white'),\n",
    "                                          ('font-weight', 'bold'),\n",
    "                                          ('text-align', 'center')]},\n",
    "        {'selector': 'tbody td', 'props': [('text-align', 'center')]},\n",
    "        {'selector': 'table', 'props': [('border-collapse', 'collapse'),\n",
    "                                        ('margin', '25px 0'),\n",
    "                                        ('font-size', '0.9em'),\n",
    "                                        ('min-width', '400px'),\n",
    "                                        ('border-radius', '5px 5px 0 0'),\n",
    "                                        ('overflow', 'hidden'),\n",
    "                                        ('box-shadow', '0 0 20px rgba(0,0,0,0.15)')]},\n",
    "        {'selector': 'tbody tr', 'props': [('border-bottom', '1px solid #dddddd')]},\n",
    "    ])\n",
    "\n",
    "    return df_table, styled_table"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e93b253",
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_ig_process_plots(baseline_data, ig_data, gadget):\n",
    "    \"\"\"Create plots showing the overhead introduced by the IG process itself\"\"\"\n",
    "    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))\n",
    "\n",
    "    # Get data for plotting\n",
    "    rps_values = baseline_data['rps'].values\n",
    "    ig_process_cpu = ig_data['ig_cpu_mean'].values\n",
    "    ig_process_mem = ig_data['ig_mem_mean'].values\n",
    "    ig_process_cpu_ci = ig_data['ig_cpu_ci'].values\n",
    "    ig_process_mem_ci = ig_data['ig_mem_ci'].values\n",
    "\n",
    "    x = np.arange(len(rps_values))\n",
    "\n",
    "    # Plot 1: IG Process CPU Usage\n",
    "    bars1 = ax1.bar(x, ig_process_cpu, alpha=0.8, color='orange',\n",
    "                    yerr=ig_process_cpu_ci, capsize=5, label='IG Process CPU')\n",
    "\n",
    "    ax1.set_xlabel('Events per Second')\n",
    "    ax1.set_ylabel('CPU Usage (%)')\n",
    "    ax1.set_title('CPU Usage (with 95% CI)')\n",
    "    ax1.set_xticks(x)\n",
    "    ax1.set_xticklabels(rps_values)\n",
    "    ax1.grid(True, alpha=0.3, axis='y')\n",
    "\n",
    "    # Set dynamic Y-axis range for CPU\n",
    "    max_cpu = np.max(ig_process_cpu)\n",
    "    ax1.set_ylim(0, max_cpu * 1.25)\n",
    "\n",
    "    # Add value labels on CPU bars\n",
    "    for i, bar in enumerate(bars1):\n",
    "        height = bar.get_height()\n",
    "        ci_value = ig_process_cpu_ci[i]\n",
    "        ax1.text(bar.get_x() + bar.get_width()/2., height + ci_value,\n",
    "                 f'{height:.2f}', ha='center', va='bottom', fontsize=9, rotation=0)\n",
    "\n",
    "    # Plot 2: IG Process Memory Usage\n",
    "    bars2 = ax2.bar(x, ig_process_mem, alpha=0.8, color='red',\n",
    "                    yerr=ig_process_mem_ci, capsize=5, label='IG Process Memory')\n",
    "\n",
    "    ax2.set_xlabel('Events per Second')\n",
    "    ax2.set_ylabel('Memory Usage (MB)')\n",
    "    ax2.set_title('Memory Usage (with 95% CI)')\n",
    "    ax2.set_xticks(x)\n",
    "    ax2.set_xticklabels(rps_values)\n",
    "    ax2.grid(True, alpha=0.3, axis='y')\n",
    "\n",
    "    # Set dynamic Y-axis range for memory\n",
    "    max_mem = np.max(ig_process_mem)\n",
    "    min_mem = np.min(ig_process_mem)\n",
    "    if max_mem > 0:\n",
    "        ax2.set_ylim(0.75*min_mem, 1.25*max_mem)\n",
    "    else:\n",
    "        ax2.set_ylim(0, 10)\n",
    "\n",
    "    # Add value labels on Memory bars\n",
    "    for i, bar in enumerate(bars2):\n",
    "        height = bar.get_height()\n",
    "        ci_value = ig_process_mem_ci[i]\n",
    "        ax2.text(bar.get_x() + bar.get_width()/2., height + ci_value,\n",
    "                 f'{height:.0f}', ha='center', va='bottom', fontsize=9, rotation=0)\n",
    "\n",
    "    # Set main title\n",
    "    fig.suptitle(f'{gadget} - IG Process', fontsize=16, fontweight='bold')\n",
    "\n",
    "    plt.tight_layout()\n",
    "\n",
    "    return fig"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5b40d4b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_performance_plots(filepath):\n",
    "    \"\"\"Create CPU and memory performance plots for a single test result file\"\"\"\n",
    "    # Read the CSV file, ignoring lines starting with #\n",
    "    df = pd.read_csv(filepath, comment='#')\n",
    "\n",
    "    # Clean column names (remove extra spaces)\n",
    "    df.columns = df.columns.str.strip()\n",
    "\n",
    "    # Get unique test names\n",
    "    test_name = df['TestName'].unique()\n",
    "    all_results = []\n",
    "\n",
    "    for test in test_name:\n",
    "        # Filter data for this gadget\n",
    "        df_test = df[df['TestName'] == test].copy()\n",
    "\n",
    "        # Aggregate the data (calculate means and CIs)\n",
    "        aggregated_df = aggregate_data(df_test)\n",
    "\n",
    "        # Separate baseline and ig data\n",
    "        baseline_data = aggregated_df[aggregated_df['Name'] == 'baseline']\n",
    "        ig_data = aggregated_df[aggregated_df['Name'] == 'ig']\n",
    "\n",
    "        if len(baseline_data) == 0 or len(ig_data) == 0:\n",
    "            print(f\"Warning: Missing baseline or ig data for gadget {test}\")\n",
    "            continue\n",
    "\n",
    "        # Set up the main figure\n",
    "        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))\n",
    "\n",
    "        # Get data for plotting\n",
    "        rps_values = baseline_data['rps'].values\n",
    "        baseline_cpu = baseline_data['%cpu'].values\n",
    "        ig_cpu = ig_data['%cpu'].values\n",
    "        baseline_cpu_ci = baseline_data['cpu_ci'].values\n",
    "        ig_cpu_ci = ig_data['cpu_ci'].values\n",
    "\n",
    "        # Plot 1: CPU Usage - simple comparison bars\n",
    "        x = np.arange(len(rps_values))\n",
    "        width = 0.35\n",
    "\n",
    "        # Baseline bar\n",
    "        bars1 = ax1.bar(x - width/2, baseline_cpu, width, label='Baseline', alpha=0.8,\n",
    "                        yerr=baseline_cpu_ci, capsize=5)\n",
    "\n",
    "        # IG Total bar\n",
    "        bars2 = ax1.bar(x + width/2, ig_cpu, width, label='With IG', alpha=0.8,\n",
    "                        yerr=ig_cpu_ci, capsize=5)\n",
    "\n",
    "        ax1.set_xlabel('Events per Second')\n",
    "        ax1.set_ylabel('CPU Usage (%)')\n",
    "        ax1.set_title('CPU Usage (with 95% CI)')\n",
    "        ax1.set_xticks(x)\n",
    "        ax1.set_xticklabels(rps_values)\n",
    "        ax1.legend()\n",
    "        ax1.grid(True, alpha=0.3, axis='y')\n",
    "\n",
    "        # Set dynamic Y-axis range for CPU\n",
    "        max_cpu = np.max(ig_cpu)\n",
    "        ax1.set_ylim(0, max_cpu * 1.25)\n",
    "\n",
    "        # Add value labels on CPU bars\n",
    "        for i, bar in enumerate(bars1):\n",
    "            height = bar.get_height()\n",
    "            ci_value = baseline_cpu_ci[i]\n",
    "            ax1.text(bar.get_x() + bar.get_width()/2., height + ci_value,\n",
    "                     f'{height:.2f}', ha='center', va='bottom', fontsize=9, rotation=0)\n",
    "\n",
    "        for i, bar in enumerate(bars2):\n",
    "            height = bar.get_height()\n",
    "            ci_value = ig_cpu_ci[i]\n",
    "            ax1.text(bar.get_x() + bar.get_width()/2., height + ci_value,\n",
    "                     f'{height:.2f}', ha='center', va='bottom', fontsize=9, rotation=0)\n",
    "\n",
    "        # Plot 2: Memory Usage - simple comparison bars\n",
    "        baseline_mem = baseline_data['mem(MB)'].values\n",
    "        ig_mem = ig_data['mem(MB)'].values\n",
    "        baseline_mem_ci = baseline_data['mem_ci'].values\n",
    "        ig_mem_ci = ig_data['mem_ci'].values\n",
    "\n",
    "        # Baseline bar\n",
    "        bars3 = ax2.bar(x - width/2, baseline_mem, width, label='Baseline', alpha=0.8,\n",
    "                        yerr=baseline_mem_ci, capsize=5)\n",
    "\n",
    "        # IG Total bar\n",
    "        bars4 = ax2.bar(x + width/2, ig_mem, width, label='With IG', alpha=0.8,\n",
    "                        yerr=ig_mem_ci, capsize=5)\n",
    "\n",
    "        ax2.set_xlabel('Events per Second')\n",
    "        ax2.set_ylabel('Memory Usage (MB)')\n",
    "        ax2.set_title('Memory Usage (with 95% CI)')\n",
    "        ax2.set_xticks(x)\n",
    "        ax2.set_xticklabels(rps_values)\n",
    "        ax2.legend()\n",
    "        ax2.grid(True, alpha=0.3, axis='y')\n",
    "\n",
    "        # Adjust Y-axis range for memory\n",
    "        all_mem_values = np.concatenate([baseline_mem, ig_mem])\n",
    "        mem_min, mem_max = np.min(all_mem_values), np.max(all_mem_values)\n",
    "        ax2.set_ylim(0.75*mem_min, 1.25*mem_max)\n",
    "\n",
    "        # Add value labels on Memory bars\n",
    "        for i, bar in enumerate(bars3):\n",
    "            height = bar.get_height()\n",
    "            ci_value = baseline_mem_ci[i]\n",
    "            ax2.text(bar.get_x() + bar.get_width()/2., height + ci_value,\n",
    "                     f'{height:.0f}', ha='center', va='bottom', fontsize=9, rotation=0)\n",
    "\n",
    "        for i, bar in enumerate(bars4):\n",
    "            height = bar.get_height()\n",
    "            ci_value = ig_mem_ci[i]\n",
    "            ax2.text(bar.get_x() + bar.get_width()/2., height + ci_value,\n",
    "                     f'{height:.0f}', ha='center', va='bottom', fontsize=9, rotation=0)\n",
    "\n",
    "        # Set main title\n",
    "        fig.suptitle(f'{test} - Host', fontsize=16, fontweight='bold')\n",
    "\n",
    "        plt.tight_layout()\n",
    "\n",
    "        # Create IG process plots\n",
    "        ig_fig = create_ig_process_plots(baseline_data, ig_data, test)\n",
    "\n",
    "        # Create table\n",
    "        df_table, styled_table = create_performance_table(baseline_data, ig_data)\n",
    "\n",
    "        all_results.append((fig, ig_fig, aggregated_df, df_table, styled_table, test))\n",
    "\n",
    "    return all_results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c30dd9ed",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Process the configured input file\n",
    "if input_file and os.path.exists(input_file):\n",
    "    print(f\"Processing file: {os.path.basename(input_file)}\")\n",
    "    try:\n",
    "        results = create_performance_plots(input_file)\n",
    "        if results:\n",
    "            for fig, ig_fig, df, df_table, styled_table, test in results:\n",
    "\n",
    "\n",
    "                # Show the system performance plot\n",
    "                plt.show()\n",
    "\n",
    "                # Show the IG process plot\n",
    "                plt.show()\n",
    "\n",
    "                # Display the styled table\n",
    "                print(f\"\\nPerformance Metrics Table - {test}:\")\n",
    "                display(styled_table)\n",
    "        else:\n",
    "            print(f\"Skipping {input_file} due to data issues\")\n",
    "    except Exception as e:\n",
    "        print(f\"Error processing {input_file}: {e}\")\n",
    "elif input_file:\n",
    "    print(f\"Error: Input file '{input_file}' not found\")\n",
    "else:\n",
    "    print(\"No input file specified. Please set the 'input_file' variable in the configuration cell.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bbb198e9",
   "metadata": {},
   "source": [
    "## Raw Data\n",
    "\n",
    "The following table shows the complete raw data from the benchmark CSV file for reference and further analysis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ec2d4153",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Display the raw CSV data as a table\n",
    "if input_file and os.path.exists(input_file):\n",
    "    try:\n",
    "        # Read the CSV file, ignoring lines starting with #\n",
    "        raw_df = pd.read_csv(input_file, comment='#')\n",
    "\n",
    "        # Clean column names (remove extra spaces)\n",
    "        raw_df.columns = raw_df.columns.str.strip()\n",
    "\n",
    "        # Style the raw data table and hide the index\n",
    "        styled_raw_table = raw_df.style.hide(axis=\"index\").set_table_styles([\n",
    "            {'selector': 'thead th', 'props': [('background-color', '#2196F3'),\n",
    "                                              ('color', 'white'),\n",
    "                                              ('font-weight', 'bold'),\n",
    "                                              ('text-align', 'center'),\n",
    "                                              ('padding', '8px')]},\n",
    "            {'selector': 'tbody td', 'props': [('text-align', 'center'),\n",
    "                                               ('padding', '6px')]},\n",
    "            {'selector': 'table', 'props': [('border-collapse', 'collapse'),\n",
    "                                            ('margin', '25px 0'),\n",
    "                                            ('font-size', '0.85em'),\n",
    "                                            ('min-width', '100%'),\n",
    "                                            ('border-radius', '5px 5px 0 0'),\n",
    "                                            ('overflow', 'hidden'),\n",
    "                                            ('box-shadow', '0 0 20px rgba(0,0,0,0.15)')]},\n",
    "            {'selector': 'tbody tr:nth-of-type(even)', 'props': [('background-color', '#f3f3f3')]},\n",
    "            {'selector': 'tbody tr', 'props': [('border-bottom', '1px solid #dddddd')]},\n",
    "        ])\n",
    "\n",
    "        # Display the styled table\n",
    "        display(styled_raw_table)\n",
    "    except Exception as e:\n",
    "        print(f\"Error displaying raw data: {e}\")\n",
    "elif input_file:\n",
    "    print(f\"Error: Input file '{input_file}' not found for raw data display\")\n",
    "else:\n",
    "    print(\"No input file specified for raw data display.\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "my-venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
